import createSwipe from '../utilities/swipe'

export default function createCarousel({
  BaseComponent,
  CLASS_PREFIX,
  Config,
  DATA_PREFIX_BASE,
  DATA_PREFIX,
  EventHandler,
  execute,
  getNextActiveElement,
  isRTL,
  isVisible,
  Manipulator,
  reflow,
  SelectorEngine,
  triggerTransitionEnd,
}) {
  const Swipe = createSwipe({
    DATA_PREFIX,
    DATA_PREFIX_BASE,
    EventHandler,
    Config,
    execute,  
  })

  /**
   * Constants
   */

  const NAME = 'carousel'
  const DATA_KEY = `${DATA_PREFIX_BASE}.carousel`
  const EVENT_KEY = `.${DATA_KEY}`
  const DATA_API_KEY = '.data-api'

  const ARROW_LEFT_KEY = 'ArrowLeft'
  const ARROW_RIGHT_KEY = 'ArrowRight'
  const TOUCHEVENT_COMPAT_WAIT = 500 // Time for mouse compat events to fire after touch

  const ORDER_NEXT = 'next'
  const ORDER_PREV = 'prev'
  const DIRECTION_LEFT = 'left'
  const DIRECTION_RIGHT = 'right'

  const EVENT_SLIDE = `slide${EVENT_KEY}`
  const EVENT_SLID = `slid${EVENT_KEY}`
  const EVENT_KEYDOWN = `keydown${EVENT_KEY}`
  const EVENT_MOUSEENTER = `mouseenter${EVENT_KEY}`
  const EVENT_MOUSELEAVE = `mouseleave${EVENT_KEY}`
  const EVENT_DRAG_START = `dragstart${EVENT_KEY}`
  const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`
  const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`

  const CLASS_NAME_CAROUSEL = `${CLASS_PREFIX}carousel`
  const CLASS_NAME_ACTIVE = `${CLASS_PREFIX}active`
  const CLASS_NAME_SLIDE = `${CLASS_PREFIX}slide`
  const CLASS_NAME_END = `${CLASS_PREFIX}carousel-item-end`
  const CLASS_NAME_START = `${CLASS_PREFIX}carousel-item-start`
  const CLASS_NAME_NEXT = `${CLASS_PREFIX}carousel-item-next`
  const CLASS_NAME_PREV = `${CLASS_PREFIX}carousel-item-prev`

  const SELECTOR_ACTIVE = `.${CLASS_PREFIX}active`
  const SELECTOR_ITEM = `.${CLASS_PREFIX}carousel-item`
  const SELECTOR_ACTIVE_ITEM = SELECTOR_ACTIVE + SELECTOR_ITEM
  const SELECTOR_ITEM_IMG = `.${CLASS_PREFIX}carousel-item img`
  const SELECTOR_INDICATORS = `.${CLASS_PREFIX}carousel-indicators`
  const SELECTOR_DATA_SLIDE = `[data-${DATA_PREFIX}slide], [data-${DATA_PREFIX}slide-to]`
  const SELECTOR_DATA_RIDE = `[data-${DATA_PREFIX}ride="carousel"]`

  const KEY_TO_DIRECTION = {
    [ARROW_LEFT_KEY]: DIRECTION_RIGHT,
    [ARROW_RIGHT_KEY]: DIRECTION_LEFT,
  }

  const Default = {
    interval: 5000,
    keyboard: true,
    pause: 'hover',
    ride: false,
    touch: true,
    wrap: true,
  }

  const DefaultType = {
    interval: '(number|boolean)', // TODO:v6 remove boolean support
    keyboard: 'boolean',
    pause: '(string|boolean)',
    ride: '(boolean|string)',
    touch: 'boolean',
    wrap: 'boolean',
  }

  /**
   * Class definition
   */

  class Carousel extends BaseComponent {
    constructor(element, config) {
      super(element, config)

      this._interval = null
      this._activeElement = null
      this._isSliding = false
      this.touchTimeout = null
      this._swipeHelper = null

      this._indicatorsElement = SelectorEngine.findOne(
        SELECTOR_INDICATORS,
        this._element,
      )
      this._addEventListeners()

      if (this._config.ride === CLASS_NAME_CAROUSEL) {
        this.cycle()
      }
    }

    // Getters
    static get Default() {
      return Default
    }

    static get DefaultType() {
      return DefaultType
    }

    static get NAME() {
      return NAME
    }

    // Public
    next() {
      this._slide(ORDER_NEXT)
    }

    nextWhenVisible() {
      // FIXME TODO use `document.visibilityState`
      // Don't call next when the page isn't visible
      // or the carousel or its parent isn't visible
      if (!document.hidden && isVisible(this._element)) {
        this.next()
      }
    }

    prev() {
      this._slide(ORDER_PREV)
    }

    pause() {
      if (this._isSliding) {
        triggerTransitionEnd(this._element)
      }

      this._clearInterval()
    }

    cycle() {
      this._clearInterval()
      this._updateInterval()

      this._interval = setInterval(
        () => this.nextWhenVisible(),
        this._config.interval,
      )
    }

    _maybeEnableCycle() {
      if (!this._config.ride) {
        return
      }

      if (this._isSliding) {
        EventHandler.one(this._element, EVENT_SLID, () => this.cycle())
        return
      }

      this.cycle()
    }

    to(index) {
      const items = this._getItems()
      if (index > items.length - 1 || index < 0) {
        return
      }

      if (this._isSliding) {
        EventHandler.one(this._element, EVENT_SLID, () => this.to(index))
        return
      }

      const activeIndex = this._getItemIndex(this._getActive())
      if (activeIndex === index) {
        return
      }

      const order = index > activeIndex ? ORDER_NEXT : ORDER_PREV

      this._slide(order, items[index])
    }

    dispose() {
      if (this._swipeHelper) {
        this._swipeHelper.dispose()
      }

      super.dispose()
    }

    // Private
    _configAfterMerge(config) {
      config.defaultInterval = config.interval
      return config
    }

    _addEventListeners() {
      if (this._config.keyboard) {
        EventHandler.on(this._element, EVENT_KEYDOWN, (event) =>
          this._keydown(event),
        )
      }

      if (this._config.pause === 'hover') {
        EventHandler.on(this._element, EVENT_MOUSEENTER, () => this.pause())
        EventHandler.on(this._element, EVENT_MOUSELEAVE, () =>
          this._maybeEnableCycle(),
        )
      }

      if (this._config.touch && Swipe.isSupported()) {
        this._addTouchEventListeners()
      }
    }

    _addTouchEventListeners() {
      for (const img of SelectorEngine.find(SELECTOR_ITEM_IMG, this._element)) {
        EventHandler.on(img, EVENT_DRAG_START, (event) =>
          event.preventDefault(),
        )
      }

      const endCallBack = () => {
        if (this._config.pause !== 'hover') {
          return
        }

        // If it's a touch-enabled device, mouseenter/leave are fired as
        // part of the mouse compatibility events on first tap - the carousel
        // would stop cycling until user tapped out of it;
        // here, we listen for touchend, explicitly pause the carousel
        // (as if it's the second time we tap on it, mouseenter compat event
        // is NOT fired) and after a timeout (to allow for mouse compatibility
        // events to fire) we explicitly restart cycling

        this.pause()
        if (this.touchTimeout) {
          clearTimeout(this.touchTimeout)
        }

        this.touchTimeout = setTimeout(
          () => this._maybeEnableCycle(),
          TOUCHEVENT_COMPAT_WAIT + this._config.interval,
        )
      }

      const swipeConfig = {
        leftCallback: () => this._slide(this._directionToOrder(DIRECTION_LEFT)),
        rightCallback: () =>
          this._slide(this._directionToOrder(DIRECTION_RIGHT)),
        endCallback: endCallBack,
      }

      this._swipeHelper = new Swipe(this._element, swipeConfig)
    }

    _keydown(event) {
      if (/input|textarea/i.test(event.target.tagName)) {
        return
      }

      const direction = KEY_TO_DIRECTION[event.key]
      if (direction) {
        event.preventDefault()
        this._slide(this._directionToOrder(direction))
      }
    }

    _getItemIndex(element) {
      return this._getItems().indexOf(element)
    }

    _setActiveIndicatorElement(index) {
      if (!this._indicatorsElement) {
        return
      }

      const activeIndicator = SelectorEngine.findOne(
        SELECTOR_ACTIVE,
        this._indicatorsElement,
      )

      activeIndicator.classList.remove(CLASS_NAME_ACTIVE)
      activeIndicator.removeAttribute('aria-current')

      const newActiveIndicator = SelectorEngine.findOne(
        `[data-${DATA_PREFIX}slide-to="${index}"]`,
        this._indicatorsElement,
      )

      if (newActiveIndicator) {
        newActiveIndicator.classList.add(CLASS_NAME_ACTIVE)
        newActiveIndicator.setAttribute('aria-current', 'true')
      }
    }

    _updateInterval() {
      const element = this._activeElement || this._getActive()

      if (!element) {
        return
      }

      const elementInterval = Number.parseInt(
        element.getAttribute(`data-${DATA_PREFIX}interval`),
        10,
      )

      this._config.interval = elementInterval || this._config.defaultInterval
    }

    _slide(order, element = null) {
      if (this._isSliding) {
        return
      }

      const activeElement = this._getActive()
      const isNext = order === ORDER_NEXT
      const nextElement =
        element ||
        getNextActiveElement(
          this._getItems(),
          activeElement,
          isNext,
          this._config.wrap,
        )

      if (nextElement === activeElement) {
        return
      }

      const nextElementIndex = this._getItemIndex(nextElement)

      const triggerEvent = (eventName) => {
        return EventHandler.trigger(this._element, eventName, {
          relatedTarget: nextElement,
          direction: this._orderToDirection(order),
          from: this._getItemIndex(activeElement),
          to: nextElementIndex,
        })
      }

      const slideEvent = triggerEvent(EVENT_SLIDE)

      if (slideEvent.defaultPrevented) {
        return
      }

      if (!activeElement || !nextElement) {
        // Some weirdness is happening, so we bail
        // TODO: change tests that use empty divs to avoid this check
        return
      }

      const isCycling = Boolean(this._interval)
      this.pause()

      this._isSliding = true

      this._setActiveIndicatorElement(nextElementIndex)
      this._activeElement = nextElement

      const directionalClassName = isNext ? CLASS_NAME_START : CLASS_NAME_END
      const orderClassName = isNext ? CLASS_NAME_NEXT : CLASS_NAME_PREV

      nextElement.classList.add(orderClassName)

      reflow(nextElement)

      activeElement.classList.add(directionalClassName)
      nextElement.classList.add(directionalClassName)

      const completeCallBack = () => {
        nextElement.classList.remove(directionalClassName, orderClassName)
        nextElement.classList.add(CLASS_NAME_ACTIVE)

        activeElement.classList.remove(
          CLASS_NAME_ACTIVE,
          orderClassName,
          directionalClassName,
        )

        this._isSliding = false

        triggerEvent(EVENT_SLID)
      }

      this._queueCallback(completeCallBack, activeElement, this._isAnimated())

      if (isCycling) {
        this.cycle()
      }
    }

    _isAnimated() {
      return this._element.classList.contains(CLASS_NAME_SLIDE)
    }

    _getActive() {
      return SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM, this._element)
    }

    _getItems() {
      return SelectorEngine.find(SELECTOR_ITEM, this._element)
    }

    _clearInterval() {
      if (this._interval) {
        clearInterval(this._interval)
        this._interval = null
      }
    }

    _directionToOrder(direction) {
      if (isRTL()) {
        return direction === DIRECTION_LEFT ? ORDER_PREV : ORDER_NEXT
      }

      return direction === DIRECTION_LEFT ? ORDER_NEXT : ORDER_PREV
    }

    _orderToDirection(order) {
      if (isRTL()) {
        return order === ORDER_PREV ? DIRECTION_LEFT : DIRECTION_RIGHT
      }

      return order === ORDER_PREV ? DIRECTION_RIGHT : DIRECTION_LEFT
    }

    // Static
    static jQueryInterface(config) {
      return this.each(function () {
        const data = Carousel.getOrCreateInstance(this, config)

        if (typeof config === 'number') {
          data.to(config)
          return
        }

        if (typeof config === 'string') {
          if (
            data[config] === undefined ||
            config.startsWith('_') ||
            config === 'constructor'
          ) {
            throw new TypeError(`No method named "${config}"`)
          }

          data[config]()
        }
      })
    }
  }

  /**
   * Data API implementation
   */

  EventHandler.on(
    document,
    EVENT_CLICK_DATA_API,
    SELECTOR_DATA_SLIDE,
    function (event) {
      const target = SelectorEngine.getElementFromSelector(this)

      if (!target || !target.classList.contains(CLASS_NAME_CAROUSEL)) {
        return
      }

      event.preventDefault()

      const carousel = Carousel.getOrCreateInstance(target)
      const slideIndex = this.getAttribute(`data-${DATA_PREFIX}slide-to`)

      if (slideIndex) {
        carousel.to(slideIndex)
        carousel._maybeEnableCycle()
        return
      }

      if (Manipulator.getDataAttribute(this, 'slide') === 'next') {
        carousel.next()
        carousel._maybeEnableCycle()
        return
      }

      carousel.prev()
      carousel._maybeEnableCycle()
    },
  )

  EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
    const carousels = SelectorEngine.find(SELECTOR_DATA_RIDE)

    for (const carousel of carousels) {
      Carousel.getOrCreateInstance(carousel)
    }
  })

  return Carousel
}
